/*
    BibleNotebook - A bible reader software
    Copyright (C) 2014  Williams Fernando Lopez

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
 */

package com.simplebible.util;

import java.io.BufferedReader;
import wl.util.Utils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 
 * @author Williams Lopez
 */
public class VirtualFolder {

    private String rootFileName;
    private RandomAccessFile dataFile;
    private RandomAccessFile infoFile;
    private List<InFile> files;
    private byte[] estadoBloque;
    private int blockLength = -1;
    private int MAXFILENAME_LENGTH = -1;
    int MAX_BLOQUES = -1;
    int MAX_FILES   = -1;
    private final byte ESTADO_BLOQUE_LIBRE = 0;
    private final byte ESTADO_BLOQUE_RESERVADO = 1;
    

    public VirtualFolder(String name, boolean readOnly) throws IOException{
        open(name,readOnly,1024,1024,102400,5000);
    }


    public VirtualFolder(String name, boolean readOnly, int blockLength, int maxFileNameLength, int maxBloques, int maxFiles) throws IOException{
        open(name,readOnly,blockLength,maxFileNameLength,maxBloques,maxFiles);
    }

    public String[] getFiles(){
        String f[] =  new String[files.size()];

        for(int i=0;i<files.size();i++){
            f[i] = files.get(i).name;
        }
        return f;
    }

    public void close() throws IOException{
        dataFile.close();
        infoFile.close();
    }

    public final void open(String name,boolean readOnly, int blockLength, int maxFileNameLength, int maxBloques, int maxFiles) throws  IOException{

        this.rootFileName = name;
        
        
        File data = new File(rootFileName);
        File info = new File(rootFileName+".files");
        
        boolean firstTime = false;

        if(!data.exists()){
            firstTime = true;
        }

        String mode = readOnly ? "r" : "rws";

        dataFile = new RandomAccessFile(data, mode);
        infoFile = new RandomAccessFile(info, mode);
        
        if(firstTime){
            estadoBloque = new byte[maxBloques];
            dataFile.write(estadoBloque);            
            infoFile.writeBytes("_config::"+blockLength+","+maxFileNameLength+","+maxBloques+","+maxFiles+"\n");//<blockLenght>,<MAXFILENAME_LENGTH>,<MAX_BLOQUES>,<MAX_FILES>
            infoFile.close();
            infoFile = new RandomAccessFile(info, mode);
        }

        readInfoFiles();
        estadoBloque = new byte[MAX_BLOQUES];
        dataFile.read(estadoBloque);
    }

    public byte[] read(String fileName) throws IOException{
        InFile file = getFile(fileName);
        if(file==null){
            throw  new IOException("Archivo no existe "+fileName);
        }
        return read(file);
    }

    private byte[] read(InFile file) throws IOException{
        Block[] bloques = getBloques(file);
        byte[] buff = new byte[size(file)];
        int index = 0;
        for(Block b: bloques){
            byte[] datos = readBlockData(b);
            for(int i=0;i<datos.length;i++){
                buff[index]=datos[i];
                index++;
            }
        }
        return buff;
    }



    public byte[] read(String fileName, int offset, int length) throws IOException{
        InFile file = getFile(fileName);
        if(file==null){
            throw  new IOException("Archivo no existe "+fileName);
        }

        //Calculando bloques inicial y final
        int flen = size(file);

        if(offset < 0 || length <=0){
            throw new IOException("Invalid length of offset Lenth("+length+") Offset("+offset+")");
        }

        if(offset > flen){
            throw new IOException("Invalid offset="+offset);
        }

        if((offset+length)>flen){
            length = (flen-offset);
        }

        int finalPosition         = offset + length;
        int bloqueInicial         = (int)(offset/blockLength);
        int bloqueFinal           = (int)((finalPosition)/blockLength);


        Block[] bloques = getBloques(file);
        byte[] buff = new byte[length];
        int index = 0;
        int indexBloque = -1;
        int pos = -1;

        for(Block b: bloques){
            indexBloque++;
            if(indexBloque < bloqueInicial){
                pos += blockLength;
                continue;
            }
            if(indexBloque > bloqueFinal){
                break;
            }

            byte[] datos = readBlockData(b);

            for (int i = 0; i < datos.length; i++) {
                pos++;
                if(pos<offset || pos>=finalPosition){ continue; }
                buff[index] = datos[i];
                index++;
            }
        }
        return buff;
    }

    public int size(String fileName) throws IOException{
        InFile file = getFile(fileName);
        if(file==null){
            throw  new IOException("Archivo no existe "+fileName);
        }
        return size(file);
    }

    private int size(InFile inf) throws IOException{
        Block[] bloques = getBloques(inf);
        int size = 0;
        for(Block b: bloques){
            size += readUsedBytes(b);
        }
        return size;
    }


    public void write(String fileName, byte[] data) throws IOException{
        truncate(fileName);
        append(fileName, data);

    }

    public void append(String fileName, byte[] data) throws IOException{
        InFile file = null;
        file = getFile(fileName);
        if(file==null){
            file = newFile(fileName);
        }
        if(file==null){
            throw  new IOException("No es posible crear el archivo");
        }

        if(data.length<0){
            return;
        }

        Block[] bloques = getBloques(file);
        
        int numBloques = (int) (data.length / blockLength);
        if (data.length % blockLength != 0) {
            numBloques += 1;
        }
        int startIn = 0, endsIn = 0;
        int dataLenght = data.length;
//        StringBuilder bloquesInfo = new StringBuilder();
//
//        for(Block b:bloques){
//            bloquesInfo.append(b.toString());
//            bloquesInfo.append(",");
//        }

        //Guardando en especio libre de bloque
        if(bloques.length>0){

            Block ultimoBloque = bloques[bloques.length-1];
            byte[] datosEnBloque =readBlockData(ultimoBloque);
            int used = datosEnBloque.length;
            int free = blockLength - used;

            if (free > 0) {
                
                if (dataLenght <= free) {
                    numBloques = 0;
                    datosEnBloque = Arrays.copyOf(datosEnBloque, datosEnBloque.length + dataLenght);

                    for(int i=0;i<data.length;i++){
                        datosEnBloque[i+used] = data[i];
                    }

                } else {
                    numBloques = (int) ((dataLenght - free) / blockLength);
                    startIn = free;
                    if (dataLenght % blockLength != 0) {
                        numBloques += 1;
                    }

                    datosEnBloque = Arrays.copyOf(datosEnBloque, blockLength);

                    for(int i=0;i<free;i++){
                        datosEnBloque[i+used] = data[i];
                    }
                }

                writeBlockData(ultimoBloque, datosEnBloque);

            }
        }

        Block[] newBloques = null;
        if (numBloques > 0) {
            newBloques = new Block[numBloques];
            for (int i = 0; i < newBloques.length; i++) {
                newBloques[i] = getBlock();
//                bloquesInfo.append(Integer.toString(bs[i].getBlockId()));
//                bloquesInfo.append(",");
                endsIn = startIn+blockLength;
                if (endsIn > dataLenght) {
                    endsIn = dataLenght;
                }
                writeBlockData(newBloques[i], Arrays.copyOfRange(data, startIn, endsIn));
                startIn = endsIn;
            }
//            writeBlockData(file.bloques, bloquesInfo.toString().getBytes());
        }

        if(newBloques !=null){
            int beforeLen = bloques.length;
            bloques = Arrays.copyOf(bloques, beforeLen+newBloques.length);
            for(int i=0;i<newBloques.length;i++){
                bloques[beforeLen+i] = newBloques[i];
            }
        }

        setBloques(file.bloques, bloques);
    }



    public void delete(String fileName) throws IOException{
        deleteFile(fileName);
    }

    public void truncate(String fileName) throws IOException{
        InFile file = getFile(fileName);
        truncate(file);
    }

    private void truncate(InFile file) throws IOException{
        if(file==null) return;
        Block[] bloques = getBloques(file);
        for(Block b: bloques){
            setBlockState(b, ESTADO_BLOQUE_LIBRE);
        }
        writeBlockData(file.bloques, new byte[0]);
    }

    public boolean exists(String fileName){
        InFile file = getFile(fileName);
        if(file == null){
            return false;
        }
        return true;
    }

    private void readInfoFiles() throws IOException{
        byte[] data = new byte[(int)infoFile.length()];
        infoFile.read(data);

        files = new ArrayList<InFile>();
        int start = 0;
        int i;
        for(i=0;i<data.length;i++){
            if(data[i]=='\n'){

                String l = new String(data, start, i - start, "UTF-8").trim();

                if (l.startsWith("_config::")) {
                    l = l.substring(9, l.length());
                    String info[] = l.split(",");
                    blockLength = Integer.parseInt(info[0]);
                    MAXFILENAME_LENGTH = Integer.parseInt(info[1]);
                    MAX_BLOQUES = Integer.parseInt(info[2]);
                    MAX_FILES = Integer.parseInt(info[3]);
                } else {

                    InFile inf = InFile.parse(l);
                    inf.start = start;
                    inf.end = i;
                    if (inf != null) {
                        files.add(inf);
                    }
                }
                start = i + 1;
            }
        }

        //Si no existe suponemos que es un archivo viejo
        //por lo que le colocamos los valores anteriores
        if(blockLength <= 0){
            blockLength = 4096;
            MAXFILENAME_LENGTH = 524288000;
            MAX_BLOQUES = 1000000;
            MAX_FILES = 500000;
        }

    }


    private InFile newFile(String name) throws IOException{
        if(exists(name)){
            throw new IOException("Archivo ya existe "+name);
        }
        if(name.length()>MAXFILENAME_LENGTH){
            throw  new IOException("Nombre de archivo muy grande");
        }

        InFile file = getFile("<deleted>");

        if(file!=null){
            file.name = name.trim();
            saveFileInfo(file);
        }else{
            long start = (int)infoFile.length();
            long end   = start;
            file  = new InFile(name.trim(),getBlock(),start,end);
            file.end = end + file.toString().length();
            infoFile.seek(start);
            infoFile.writeBytes(file.toString()+"\n");
            files.add(file);
        }
        return file;
    }

    private void saveFileInfo(InFile f) throws IOException{
        infoFile.seek(f.start);
        infoFile.writeBytes(f.toString());
    }

    private void writeBlockData(Block b,byte[] datos) throws IOException{

        if(datos.length > blockLength){
            throw  new IOException("Datos exceden el limite del bloque ["+datos.length+">"+blockLength+"]");
        }
        dataFile.seek(getBlockPosition(b.id));
        dataFile.writeBytes(Utils.fillWithCero(datos.length, 4));
        if(datos.length > 0){
            dataFile.write(datos);
        }
    }

    private Block[] getBloques(InFile inf) throws IOException{
//        byte[] datos = readBlockData(inf.bloques);
//        String[] sl = (new String(datos)).split(",");
//        List<Block> bloques = new ArrayList<Block>();
//        for(String s : sl){
//            if(s.trim().equals("")) continue;
//            Block b = Block.parse(s.trim());
//            if(b!=null){
//                bloques.add(b);
//            }
//        }
//
//        if(bloques.isEmpty()){
//            return new Block[0];
//        }
//
//        Block[] barray = new Block[bloques.size()];
//        barray=bloques.toArray(barray);
//        return barray;


        Block current = inf.bloques;
        List<Block> bloques = new ArrayList<Block>();

        for (int i = 0; i < 1000; i++) {

            if(current == null) break;

            byte[] datos = readBlockData(current);
            String[] sl = (new String(datos)).split(",");
            current = null;

            for (int j=0;j<sl.length;j++) {
                
                String s = sl[j].trim();
                if (s.equals("")) {
                    continue;
                }
                if (s.equals("next")){
                    current = Block.parse(sl[j+1]);
                    break;
                }
                Block b = Block.parse(s.trim());
                if (b != null) {
                    bloques.add(b);
                }
            }
        }        

        if(bloques.isEmpty()){
            return new Block[0];
        }

        Block[] barray = new Block[bloques.size()];
        barray=bloques.toArray(barray);
        return barray;


    }

    private void setBloques(Block blockData,Block[] bloques) throws IOException{
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Block currentBlockData = blockData;

        for(Block b:bloques){
            byte[] serializedBlock = (Integer.toString(b.id)+",").getBytes();
            if((baos.size()+serializedBlock.length+14) > blockLength){
                Block nextBlockData = getBlock();
                baos.write(("next,"+nextBlockData.toString()).getBytes());
                writeBlockData(currentBlockData, baos.toByteArray());
                currentBlockData = nextBlockData;
                baos.reset();
            }
            baos.write(serializedBlock);
        }

        writeBlockData(currentBlockData, baos.toByteArray());

    }

    private byte[] readBlockData(Block b) throws IOException{
        long pos = getBlockPosition(b.id);
        byte[] blen = new byte[4];
        dataFile.seek(pos);
        dataFile.read(blen);
        int len = Integer.parseInt(new String(blen));
        byte[] data = new byte[len];
        dataFile.read(data);
        return data;
    }

    private byte[] readBlockData(Block b, int offset, int length) throws IOException{

        long pos = getBlockPosition(b.id);
        byte[] blen = new byte[4];
        dataFile.seek(pos);
        dataFile.read(blen);
        int len = Integer.parseInt(new String(blen));


        if(offset > len){
            throw new IOException("Offset out off range "+offset+">"+len);
        }

        if(offset + length > len){
            length = (len-offset);
        }

        if(length <= 0 || offset < 0){
            throw new IOException("Ilegal range offset="+offset+" o length="+length);
        }

        dataFile.seek(dataFile.getFilePointer()+offset);
        byte[] data = new byte[length];
        dataFile.read(data);
        return data;
    }

    private int readUsedBytes(Block b) throws IOException{
        long pos = getBlockPosition(b.id);
        byte[] blen = new byte[4];
        dataFile.seek(pos);
        dataFile.read(blen);
        int len = Integer.parseInt(new String(blen));
        return len;
    }
    

    private void deleteFile(String name) throws IOException{
        InFile f = getFile(name);

        if(f==null){
            return;
        }
        truncate(f);
        f.name = "<deleted>";
        saveFileInfo(f);
    }

    private InFile getFile(String name){
        if(files == null){
            return null;
        }
        for(InFile f : files){
            if(f.name.equalsIgnoreCase(name)){
                return f;
            }
        }
        return null;
    }

    private Block getBlock() throws IOException{

        int id = -1;

        for(int i=0;i<estadoBloque.length;i++){
            if(estadoBloque[i]==0){
                id =i;
                break;
            }
        }

        if(id == -1){
            throw new IOException("Limite de bloques de archivo alcanzado");
        }


        Block b = new Block();
        b.id = id;

        long pos = getBlockPosition(id);

        if(pos >= dataFile.length()){
            dataFile.seek(pos);
            dataFile.write(blockLength+4);
        }

        setBlockState(b, ESTADO_BLOQUE_RESERVADO);

        writeBlockData(b, new byte[0]);
        
        return b;
    }

    private void setBlockState(Block b, byte state) throws IOException{
        estadoBloque[b.id] = state;
        long pos = getBlockPosition(b.id);
        dataFile.seek(b.id);
        dataFile.writeByte(state);
    }

    private long getBlockPosition(int id){
        return id*(blockLength+4)+estadoBloque.length;
    }


}


class InFile{
    long start, end;
    String name;
    Block bloques;
    boolean deleted = false;


    public InFile(String name, Block bloques, long start, long end){
        this.start = start;
        this.end = end;
        this.name = name;
        this.bloques = bloques;
    }

    private InFile(){

    }

    public static InFile parse(String str){
        try{
            if(str == null || str.equals("")){
                return null;
            }

            InFile inf = new InFile();

            if(str.startsWith("<deleted>")){
                inf.deleted  = true;
            }

            String[] fd = str.split(";");
            inf.name = fd[0].trim();
            inf.bloques = Block.parse(fd[1].trim());
            return inf;
        }catch(Exception e){
            return null;
        }
    }

    @Override
    public String toString() {

        String n = name;

        for(int i=name.length();i<30;i++){
            n+=' ';
        }
        return n + ";" + bloques;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final InFile other = (InFile) obj;
        if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 89 * hash + (this.name != null ? this.name.hashCode() : 0);
        return hash;
    }




}

class Block{
    int  id;

    public static Block parse(String str){
        str = str.trim();
        Block b = new Block();
        b.id = Integer.parseInt(str.trim());
        return b;
    }

    @Override
    public String toString() {
        return Utils.fillWithCero(id, 9);
    }

    public int getBlockId(){
        return id;
    }
}



